Skip to content

plan 0015: Drag mode — distance runs without a track - #148

Merged
TheAngryRaven merged 2 commits into
BETAfrom
claude/drag-mode-planning-mggk0b
Aug 26, 2026
Merged

TheAngryRaven merged 2 commits into
BETAfrom
claude/drag-mode-planning-mggk0b

Conversation

@TheAngryRaven

@TheAngryRaven TheAngryRaven commented Aug 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds Drag mode: main menu → Drag → pick a distance (1/8 Mile, 1000 ft, 1/4 Mile, 1/2 Mile, 1 Mile) and the session starts — no track file, no detection. The device stages at a standstill, starts the clock rollout-style (11.25 in of movement past a re-latching standstill anchor, ET start interpolated between 25 Hz fixes), and ends the run at the target distance with an interpolated ET + trap speed and a 0-60 mph split. It re-arms automatically when the car stops, so a whole day of passes lands in one DOVEX session (race_mode=DRAG, laps line = run ETs — same backwards-compatible trailing-column scheme as SPRINT; trap/0-60 stay out of the frozen header since the 25 Hz rows carry speed).

The run state machine is a new host-tested pure unit, drag_timer.{h,cpp}: staging (≤1 mph held 1 s, anchor = re-latching running mean so staging-lane GPS drift can't fake a launch), launch (rollout + ≥2 mph, with a ≥2 s fix-gap guard on the launch edge so a launch inside a GPS dropout can't anchor its ET to a parked-car fix), a prove-out gate (reach 15 mph within 5 s of the ET start or the run is abandoned — a wave-off driven to the pits is not a pass), chord-distance accumulation, and silent aborts (3 s mid-run standstill, ≥2 s fix gap) — which is also how queue-creep phantom launches self-cancel. Time base is Unix epoch ms (getGpsUnixTimestampMillis()), never the time-of-day clock that wraps at UTC midnight, and a backwards time step aborts + resyncs rather than wedging the stream. The sketch keeps only sprint-style glue: dragTimer != nullptr IS drag mode, drag branch first in every activeTimer*() helper, run capture on the run-count edge, and the auto-idle grace re-arms on each completed run and each fresh stage so a staging queue never idles the session out. Design record in docs/plans/0015-drag-mode.md.

UI: new Drag main-menu row + PAGE_DRAG_DISTANCE picker (scrolling 3-row window, Back row); the Current Lap / Pace / Best Lap pages branch for drag (live ET, *staged*/*waiting*, trap/0-60 subtext, live 0-60 readout); the LED pace pip is suppressed between runs like sprint.

Rebased onto current BETA (post-4.1.0 cut) and renumbered from the colliding plan 0011 to plan 0015. A full self-review of the branch produced 6 findings (3 correctness — the midnight time-base wedge, the launch-edge gap, the wave-off junk run — plus a feet/meters accessor inconsistency, a duplicated haversine, and duplicated 0-60 formatting); all 6 are fixed in the second commit, each with a regression test where testable.

Follow-up outside this repo: the webapp will want a DRAG case in its race_mode loader (old viewers degrade gracefully to "unknown mode with a runs line").

Type of change

  • Bug fix (no user-visible behavior change beyond the fix)
  • New feature / behavior
  • Refactor (no behavior change)
  • Tests only
  • CI / tooling / docs
  • Breaking change (track files, log format, BLE protocol, or a removed mode)

How it was verified

  • Host unit tests pass (ctest --test-dir tests/build) — 587 cases on the rebased tree, including 19 drag_timer cases (analytic launch/ET/trap/0-60 interpolation, anchor-drift and jitter immunity, abort paths, re-arm cycles, backwards-time-step recovery, staged-gap launch, wave-off prove-out, distance-table goldens)
  • clang-tidy clean — left to CI
  • Compiles for the XIAO nRF52840 Sense — left to CI (native sim build of the full firmware TU compiles clean, built against DovesLapTimer BETA)
  • Tested on real hardware — not yet; sim-verified: boot soak, determinism, golden fixtures (menu walk updated for the new row + a new picker fixture, goldens regenerated), both lap oracles, and two-session carryover all pass

Checklist

  • CHANGELOG.md updated under [Unreleased] (if user-visible)
  • ARCHITECTURE.md / CLAUDE.md updated (if a module or interface changed)
  • New testable logic has a matching test in tests/
  • Branch is focused — refactors / behavior / tests are not mixed together

Related issues

None — design record: docs/plans/0015-drag-mode.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BKa62h4fhn61pKn1LygrMK

@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Coverage — host-testable units

📂 Overall coverage

Metric Coverage
Lines 🟢 2001/2032 (98.5%)
Functions 🟢 207/207 (100.0%)
Branches 🟡 1446/1608 (89.9%)

📄 File coverage

File Lines Functions Branches
BirdsEye/ble_stream.cpp 🟢 34/34 (100.0%) 🟢 8/8 (100.0%) 🟡 17/20 (85.0%)
BirdsEye/camera_fsm.cpp 🟢 238/246 (96.7%) 🟢 20/20 (100.0%) 🟡 142/160 (88.8%)
BirdsEye/course_creator.cpp 🟢 213/221 (96.4%) 🟢 21/21 (100.0%) 🟡 119/136 (87.5%)
BirdsEye/course_prune.cpp 🟢 37/37 (100.0%) 🟢 5/5 (100.0%) 🟢 47/50 (94.0%)
BirdsEye/crc32.cpp 🟢 30/30 (100.0%) 🟢 4/4 (100.0%) 🟢 24/24 (100.0%)
BirdsEye/crossing_pattern.cpp 🟢 15/15 (100.0%) 🟢 1/1 (100.0%) 🟢 12/12 (100.0%)
BirdsEye/dovex_header.cpp 🟢 106/107 (99.1%) 🟢 7/7 (100.0%) 🔴 62/88 (70.5%)
BirdsEye/drag_timer.cpp 🟢 147/151 (97.4%) 🟢 10/10 (100.0%) 🟡 74/92 (80.4%)
BirdsEye/filename_validator.cpp 🟢 14/14 (100.0%) 🟢 1/1 (100.0%) 🟢 30/30 (100.0%)
BirdsEye/gps_stats.cpp 🟢 25/25 (100.0%) 🟢 3/3 (100.0%) 🟢 8/8 (100.0%)
BirdsEye/gps_status_page.cpp 🟢 29/29 (100.0%) 🟢 4/4 (100.0%) 🟢 28/28 (100.0%)
BirdsEye/gps_time.cpp 🟢 45/45 (100.0%) 🟢 6/6 (100.0%) 🟢 30/32 (93.8%)
BirdsEye/gps_validation.cpp 🟢 24/24 (100.0%) 🟢 2/2 (100.0%) 🟢 66/66 (100.0%)
BirdsEye/haversine.cpp 🟢 8/8 (100.0%) 🟢 1/1 (100.0%) ⚫ 0/0 (0.0%)
BirdsEye/idle_policy.cpp 🟢 17/17 (100.0%) 🟢 2/2 (100.0%) 🟢 14/14 (100.0%)
BirdsEye/insta360_protocol.cpp 🟢 140/140 (100.0%) 🟢 16/16 (100.0%) 🟡 86/98 (87.8%)
BirdsEye/lap_format.cpp 🟢 18/18 (100.0%) 🟢 1/1 (100.0%) 🟢 9/9 (100.0%)
BirdsEye/led_animations.cpp 🟢 84/84 (100.0%) 🟢 7/7 (100.0%) 🟢 43/46 (93.5%)
BirdsEye/led_frame.cpp 🟢 21/21 (100.0%) 🟢 7/7 (100.0%) 🟢 6/6 (100.0%)
BirdsEye/led_modes.cpp 🟢 67/68 (98.5%) 🟢 6/6 (100.0%) 🟢 50/52 (96.2%)
BirdsEye/led_status.cpp 🟢 106/108 (98.1%) 🟢 11/11 (100.0%) 🟢 71/75 (94.7%)
BirdsEye/local_time.cpp 🟢 48/48 (100.0%) 🟢 6/6 (100.0%) 🟢 46/50 (92.0%)
BirdsEye/loop_profile.cpp 🟢 65/65 (100.0%) 🟢 7/7 (100.0%) 🟢 35/36 (97.2%)
BirdsEye/sat_bars.cpp 🟢 33/33 (100.0%) 🟢 2/2 (100.0%) 🟢 51/54 (94.4%)
BirdsEye/sd_access_policy.cpp 🟢 9/9 (100.0%) 🟢 3/3 (100.0%) 🟢 18/18 (100.0%)
BirdsEye/sd_format_page.cpp 🟢 25/25 (100.0%) 🟢 3/3 (100.0%) 🟢 25/26 (96.2%)
BirdsEye/sector_purple.cpp 🟢 84/85 (98.8%) 🟢 3/3 (100.0%) 🟡 57/64 (89.1%)
BirdsEye/sensoregg_protocol.cpp 🟢 44/45 (97.8%) 🟢 7/7 (100.0%) 🟢 33/34 (97.1%)
BirdsEye/setting_parse.cpp 🟢 29/30 (96.7%) 🟢 2/2 (100.0%) 🟢 38/42 (90.5%)
BirdsEye/sprint_select.cpp 🟢 25/25 (100.0%) 🟢 4/4 (100.0%) 🟢 46/48 (95.8%)
BirdsEye/tach_filter.cpp 🟢 91/91 (100.0%) 🟢 13/13 (100.0%) 🟡 72/82 (87.8%)
BirdsEye/track_json.cpp 🟢 116/120 (96.7%) 🟢 12/12 (100.0%) 🟡 67/88 (76.1%)
BirdsEye/wake_cause.cpp 🟢 14/14 (100.0%) 🟢 2/2 (100.0%) 🟢 20/20 (100.0%)

TheAngryRaven pushed a commit that referenced this pull request Aug 24, 2026
Three design records landed in the same mass-merge window all claiming
plan number 0012. Renumber by merge order into BETA, so the earliest
merge keeps the number:

  #156  0012-download-throughput-regression-deepdive  (keeps 0012)
  #155  0012-led-status-modes            -> 0013-led-status-modes
  #154  0012-tach-rpm-single-pickup     -> 0014-tach-rpm-single-pickup

Every in-tree citation follows: each line referencing "0012" was
attributed to the merge that introduced it via first-parent blame (so
prose like "plan 0012" and "pre-0012" lands with the right plan), then
rewritten - code comments, CLAUDE.md, CONTRIBUTING.md, the CHANGELOG's
two slug citations, the compile-sketch.yml flags-off comment, test
comments and test-case names, and plan 0003's superseded-by link. The
deepdive's own references stay 0012 untouched. No behavior change;
host suite still 567/567.

Commit messages citing "plan 0012" remain ambiguous - history cannot be
rewritten - but the files they resolve to are now unique. PR #148 (drag
mode) also styles itself plan 0011, which profiling holds; it should
take 0015, the next free number, when it lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqSstYTWPhm3TZ3PxXs4hk
Main menu -> Drag -> pick 1/8 Mile / 1000 ft / 1/4 Mile / 1/2 Mile /
1 Mile and the session starts: no track file, no detection. Stage at a
standstill, rollout-style launch (11.25 in past a re-latching standstill
anchor, ET start interpolated between 25 Hz fixes), run ends at the
target distance with interpolated ET + trap speed and a 0-60 split, then
re-arms automatically for the next pass. All passes land in one DOVEX
session with race_mode=DRAG (laps line = run ETs, same trailing-column
scheme as SPRINT).

- New host-tested pure unit drag_timer.{h,cpp}: the whole run state
  machine (ARMED/STAGED/LAUNCHED), the distance table, silent aborts
  (mid-run standstill, >=2 s fix gap — also how queue-creep phantom
  launches self-cancel), 14 doctest cases.
- Sketch glue mirrors sprint: dragTimer != nullptr IS drag mode, drag
  branch first in every activeTimer*() helper, run capture on the
  run-count edge, trackDetected latched, idle-grace re-arm on each run
  AND each fresh STAGED latch.
- UI: Drag main-menu row, PAGE_DRAG_DISTANCE picker (scrolling window),
  drag branches on the lap-time/pace/best pages (live ET, *staged*,
  trap/0-60 subtext, live 0-60 readout), LED pace pip suppressed between
  runs like sprint.
- Sim: golden walk updated for the new menu row + a picker fixture;
  goldens regenerated; soak/determinism/goldens/oracles all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKa62h4fhn61pKn1LygrMK
@TheAngryRaven
TheAngryRaven force-pushed the claude/drag-mode-planning-mggk0b branch from 73019ef to 64246e7 Compare August 26, 2026 00:28
@TheAngryRaven TheAngryRaven changed the title plan 0011: Drag mode — distance runs without a track plan 0015: Drag mode — distance runs without a track Aug 26, 2026
Six findings from the branch review, all fixed:

- Time base: the glue fed getGpsTimeInMilliseconds() (time-of-day, wraps
  to zero at UTC midnight — evening sessions) into a unit whose contract
  says epoch ms. Both call sites now feed getGpsUnixTimestampMillis(),
  and the unit's non-monotonic guard resyncs on a backwards step instead
  of rejecting every subsequent fix — one step used to wedge the drag
  timer for the rest of the session.
- Launch edge: a >=2 s fix gap while STAGED now re-stages instead of
  evaluating the launch, so a launch that happened inside a GPS dropout
  can't interpolate its ET start back to a parked-car fix.
- Prove-out gate: a launch must reach 15 mph within 5 s of the ET start
  or the run is silently abandoned — a wave-off driven to the pits at
  4 mph never holds the sub-2 mph standstill abort and used to record
  660 ft of pit road as a ~90 s "run".
- activeTimerTotalDistance() converts the drag branch to meters, matching
  every other branch of the accessor.
- distanceFeet() delegates to the haversine unit (x 5280) instead of
  carrying a second copy of the formula and Earth radius.
- The seconds.hundredths split rendering is now one function
  (displayPrintSplitSeconds) shared by the results subtext and the pace
  page's live 0-60 readout.

Five new regression tests cover the wedge recovery, the staged-gap
launch, the wave-off, a slow-but-real pass, and duplicate timestamps.
Plan 0015 doc + CLAUDE.md constants updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BKa62h4fhn61pKn1LygrMK
@TheAngryRaven
TheAngryRaven merged commit 458e596 into BETA Aug 26, 2026
9 checks passed
@TheAngryRaven TheAngryRaven mentioned this pull request Sep 27, 2026
11 of 18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants